Fix hosted gem redirect ignoring Bundler mirror.all (#681) - #684
Mikola Lysenko (mikolalysenko) wants to merge 10 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
Bundler's mirror.all (or a mirror for the patch-registry source) sends the per-dep source block the hosted redirect writes to the mirror, which serves the unpatched upstream gem. The scan reported the gem redirected and the in-run VEX attested not_affected while the next bundle install was unpatched or failed CHECKSUMS. The hosted intake now reads the mirror settings from the bundler app config and BUNDLE_MIRROR__ALL and, when one captures the patch registry, leaves the Gemfile pair untouched, attests nothing, and warns redirect_gem_mirror_overrides_source with the remedy (scope the mirror to rubygems.org). Fixes #681 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
The redirect_gem_mirror_overrides_source detail always advised unsetting a local mirror.all, which never clears a BUNDLE_MIRROR__ALL from the environment or a mirror.<source> key for the patch registry. The mirror model now returns the remedy for the setting it detected, and a test applies each remedy and checks the next scan passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEpjVvY7X41jPuVuoiCLVz
|
Generated by Claude Code |
|
bugbot run Generated by Claude Code |
|
Ready for review at head
Slack announcement not sent: this run has no Slack send tool, so the next run should retry. Generated by Claude Code |
|
Codex follow-up review of The older hosted gem pin finding is independently confirmed: an empty catalog or empty grants bypass mirror detection and can still yield a false attestation. A further correction is in progress. The Windows case-handling finding was investigated and dismissed using the tagged Ruby/Bundler source chain; the thread explains why OS lookup semantics do not apply to Bundler's case-sensitive snapshot. No native Windows test is claimed. The validation below covers the correction already pushed, and does not establish safety for the newly confirmed candidate-gap case. The original P1 had several paths to a false hosted attestation: hostname/app and per-source environment mirrors escaped detection; an empty The correction models effective app/environment mirror precedence and all/source/hostname matching, including Bundler key normalization and fallback-only shadowing. Empty Validation on the exact committed source:
User-global Bundler config and mirror settings introduced only in a later install environment remain documented limits. Standalone VEX behavior is unchanged. This is focused local validation, not a full workspace/platform rerun. Ready for review remains off while these findings are verified and resolved, and until complete CI and Bugbot are clear. |
|
Cursor (@cursor) review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: VEX misses mirrors without gem candidates
- Added independent mirror detection that checks for capturing mirrors even when no gem candidates are present, preventing false attestations for lockfile-discovered gems.
Or push these changes by commenting:
@cursor push d702d51261
Preview (d702d51261)
diff --git a/crates/socket-patch-cli/src/commands/scan/hosted.rs b/crates/socket-patch-cli/src/commands/scan/hosted.rs
--- a/crates/socket-patch-cli/src/commands/scan/hosted.rs
+++ b/crates/socket-patch-cli/src/commands/scan/hosted.rs
@@ -1253,10 +1253,25 @@
// rediscovers older pins too, so a candidate-only set would miss some
// refused hosted gems. Keep their actual installed-byte verification,
// but do not infer applied status from the intercepted source.
- params.hosted_gem_mirror_refused = rewrite
- .warnings
- .iter()
- .any(|warning| warning.code == "redirect_gem_mirror_overrides_source");
+ // Check for mirrors independently of whether gem candidates are
+ // present: a lockfile-discovered gem pin can still be affected by a
+ // capturing mirror even when this run has no gem grants. Probe for
+ // mirror.all and hostname/exact-source mirrors that would capture the
+ // patch registry, using a representative source URL.
+ params.hosted_gem_mirror_refused = {
+ let patch_registry_sources = &["https://patch.socket.dev/gem/"];
+ let mirror_detected = socket_patch_core::crawlers::ruby_crawler::bundler_source_mirror(
+ &common.cwd,
+ patch_registry_sources,
+ )
+ .await
+ .is_some();
+ mirror_detected
+ || rewrite
+ .warnings
+ .iter()
+ .any(|warning| warning.code == "redirect_gem_mirror_overrides_source")
+ };
// Stale-flagged purls are EXCLUDED from assume_applied: the same-run
// envelope carries a redirect_gem_stale_install warning proving the
// installed materialization unpatched, so attesting that purl fromYou can send follow-ups to the cloud agent here.
The mirror refusal flag for embedded VEX was derived only from the rewrite's redirect_gem_mirror_overrides_source warning, which exists only when this run had gem candidates. A hosted scan with an empty catalog, a paid-only gem or a withdrawn offer still rediscovers older hosted gem pins in its VEX plan, so lockfile inference and --vex-no-verify could attest them while Bundler fetched unpatched bytes through a capturing mirror. Embedded hosted VEX now checks each hosted gem pin in the completed plan against the project's Bundler mirror settings (using the pin's own Socket source), on the redirect path and on the hosted scan's empty JSON and human terminal paths. Verified installed bytes remain valid evidence; standalone and agent/vendored VEX are unchanged. Co-Authored-By: Claude <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
|
[burn-down agent] Ready for review at head
Slack announcement not sent this run (Slack send tool unavailable). Generated by Claude Code |
Keep the #681 mirror gate alongside main's new hosted gem work: the memory-view classify call gains main's global-config argument, the VEX lockfile excuse keeps both the mirror refusal and main's pnpm hidden-store check, the e2e driver lists carry both the mirror and declaration variants, and the docs keep both the mirror refusal text and main's global ~/.bundle/config and requirements takeover notes. The mirror crawler tests pass None for main's new global-config parameter. Co-Authored-By: Claude <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
main went red when #605 taught the name-keyed resolver to find pnpm store copies, which the #738 alias tests assumed it missed. Same change as #851; it no-ops once main carries it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEpjVvY7X41jPuVuoiCLVz
|
I copied #851's test-only change onto this branch as e1eaa79. It becomes a no-op once #851 lands on Generated by Claude Code |
|
bugbot run Generated by Claude Code |
|
Burn-down agent: labeled Ready for review at
Generated by Claude Code |
Resolved conflicts: - CLI_CONTRACT.md: kept main's new Gradle confirmation sentence and this branch's redirect_gem_mirror_overrides_source code description. - hosted/engine.rs: kept main's new CandidateFiles::gradle_unreadable field alongside this branch's gem_refusal (which replaces main's gem_manifest_unsupported). keep_bundler_loaded_gem_files now uses main's bundler_loaded_manifest_in(view) for the loaded manifest and keeps this branch's mirror probe (bundler_source_mirror on disk, capturing_mirror on the in-memory app config) and refusal logic. - tests/e2e_redirect_gem_build.rs: kept both sets of new drivers (ScanVexMirror* from this branch, ScanVexCustomGitSource from main) in the enum, expected-code match and redirect arm. Co-Authored-By: Claude <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
main went red when Gradle code landed with inline sha1/sha256 calls that utils::digest::tests::production_digests_go_through_the_helpers rejects. Same change as #878; it no-ops once main carries it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEpjVvY7X41jPuVuoiCLVz
|
I copied #878's change onto this branch as 745898a. It routes those calls through Generated by Claude Code |
|
bugbot run Generated by Claude Code |
|
Burn-down agent: labeled Ready for review at
Generated by Claude Code |
Brings in the three commits that landed on main since the last merge so CI, including the CodeQL default setup whose previous run was cancelled, runs again against the current base. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 0e1e6cd. Configure here.
|
Burn-down agent: Ready for review re-confirmed at head
Generated by Claude Code |


LLM Description written by Claude Code:claude-opus-5-5
Fixes #681
Summary
Hosted gem scans refuse redirects when the effective Bundler mirror would capture the Socket patch-registry source. The refusal preserves the manifest and lockfile and emits
redirect_gem_mirror_overrides_sourcewithout exposing private mirror credentials. Embedded VEX then requires verified installed bytes for hosted gems, so a previous hosted pin cannot falsely attest a missing or upstream installation.Root cause
Bundler can capture a per-dependency source with an all-source, exact-source, or hostname mirror. The original implementation missed hostname and source-specific environment settings, treated an empty
BUNDLE_APP_CONFIGas unset, and let independent VEX lockfile inference bypass a detected refusal on later scans. Automatic warning/remedy text also copied credentials from mirror URLs.Fix
BUNDLE_APP_CONFIGandBUNDLE_IGNORE_CONFIGconsistently with the shared Ruby intake. An explicitly empty app-config path selects the project-rootconfigfile. Environment mirror keys retain their literal native interpretation.Known limits: user-global
~/.bundle/configremains outside this reader; mirror settings introduced only in a later install environment cannot be observed during the scan. A capturing mirror conservatively refuses all gem redirects in the run.Validation of the correction
scan/hosted.rsformatting is retained. These are focused local checks, not a full workspace/platform rerun.Author validation on the original commit
468a237Historical evidence below is from the original implementation on Linux, Ruby 3.3.6, Bundler 4.0.17, and toolchain 1.93.1. It does not establish CI status for the correction.
gem_hosted_bundler_mirror_all_redirects_nothingFAILED with"redirect":{"redirected":1,"rewrittenFiles":["Gemfile"],…},"vex":{"statements":1,…}, the exact symptom in Hosted gem redirect ignores Bundler'smirror.allsetting, so the nextbundle installfetches the redirected gem's upstream bytes from the mirror while the in-run VEX attestsnot_affected#681.bundler_mirror_all_redirects_nothingandbundler_mirror_for_the_patch_source_redirects_nothingalso FAILED.cargo test -p socket-patch-cli --all-features --test e2e_redirect_gem_build -- --ignored: 12/12 pass, including the new test and every existing hosted gem capstone.cargo clippy --workspace --all-features -- -D warnings: clean. The new files are rustfmt-clean.mainitself isn't rustfmt-clean and CI doesn't gate on it, so unrelated files were left alone.cargo test --workspace --all-features --no-fail-fast: 214 suites ok. 12 tests fail only because this sandbox runs as uid 0, wherechmod 0555and unremovable-file injections can't force the write failures they test (*_state_write_failure_*,*unremovable*,relax_loop_must_not_traverse_symlinked_root,wire_write_failure_*). None of them touches gem or hosted-intake code. CI runs them as a normal user.CI
0e1e6cdmergesmain(9c43dfc, including #712's gem VEX change) into this branch. The merge was textually clean. It's needed because the CodeQL default-setup run on745898awas cancelled and GitHub refuses to retrydynamicruns.Local checks on
0e1e6cd:cargo clippy --workspace --all-features -- -D warningsis clean.socket-patch-core --libpassed 5260 tests; the 4 failures are the uid-0 write-failure tests already listed above.socket-patch-cli --libpassed 849/849.e2e_redirect_gem_build(Ruby 3.3.6 / Bundler 4.0.17, with ignored tests included),e2e_redirect_gem_stale_install,e2e_vex_redirect,e2e_embedded_vexandcovgap_commands_vexpassed 119/119. A full--workspacerun didn't fit in this sandbox's disk allowance, so CI covers the rest.CI on
0e1e6cd: all 14 workflows and CodeQL are green. Bugbot found one issue (IPv6 hostname mirrors). It was answered as a false positive with evidence, since Ruby'sURI#hostkeeps the brackets (checked:URI("https://[::1]:8443/").host == "[::1]"), and the thread is resolved.🤖 Generated with Claude Code
https://claude.ai/code/session_01NEpjVvY7X41jPuVuoiCLVz
Note
Medium Risk
Changes hosted gem redirect and VEX attestation rules for Bundler mirrors; incorrect mirror modeling could still refuse valid projects or miss unsafe configs, but behavior is conservative and well-tested.
Overview
Hosted gem redirects now fail closed when Bundler mirror settings (
mirror.all, exact patch-source, or hostname mirrors in app config orBUNDLE_MIRROR__*env) would route the Socket patch-registry source to an upstream mirror. The hosted engine drops gem manifest/lock candidates and emitsredirect_gem_mirror_overrides_sourcewith remediation text that does not echo mirror URLs or credentials.A new
formats/gem/mirrormodel (wired throughruby_crawler::bundler_source_mirror) matches Bundler precedence and key normalization;BUNDLE_APP_CONFIGhandling now treats an empty value like Ruby (project-rootconfiginstead of default.bundle).Embedded VEX on hosted scans sets
hosted_gem_mirror_check(andhosted_gem_mirror_refusedwhen that warning fired) so rediscovered hosted gem pins cannot attest via lockfile inference,assume_applied, or--vex-no-verifywhen a mirror captures the source; verified installed bytes still count. Failures surface asmirror_overrides_source.Docs (CLI_CONTRACT, ecosystems.md) and e2e/unit tests cover mirror refusal, credential-free diagnostics, and rescan behavior. Unrelated digest call sites consolidate on
utils::digesthelpers.Reviewed by Cursor Bugbot for commit 745898a. Configure here.
Generated by Claude Code